package com.htfyun.testa2recorder;

import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import com.blankj.utilcode.util.FileUtils;
import com.chad.library.adapter.base.BaseQuickAdapter;
import com.chad.library.adapter.base.viewholder.DataBindingHolder;
import com.htfyun.omrecorder.AudioChunk;
import com.htfyun.omrecorder.AudioRecordConfig;
import com.htfyun.omrecorder.OmRecorder;
import com.htfyun.omrecorder.PullTransport;
import com.htfyun.omrecorder.PullableSource;
import com.htfyun.omrecorder.Recorder;
import com.htfyun.omrecorder.WavHeader;
import com.htfyun.testa2recorder.databinding.ActivityTestMultiMicBinding;
import com.htfyun.testa2recorder.databinding.ItemMicPlayMonoBinding;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class TestMultiMicActivity extends AppCompatActivity {
    private ActivityTestMultiMicBinding binding;

    private String APP_PATH;
    private Recorder recorder = null;

    private final PCMConfig DEFAULT_PCM_CONFIG_4CH_32K_32B = new PCMConfig(4, 32000, 32);//4通道, 32k, 32bit
    private final PCMConfig DEFAULT_PCM_CONFIG_2CH_16K_16B = new PCMConfig(2, 16000, 16);//2通道, 16k, 16bit

    private int audioRate;
    private int channels;//用于解析 录音数据用的. AudioRecord 里没有用到这个.

    private OutputStream[] outs;
    private OutputStream[] outsCheck;
    private PCMConfig pcmConfig;

    private BaseQuickAdapter<PlayItem, DataBindingHolder<ItemMicPlayMonoBinding>> adapter;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        APP_PATH = getFilesDir().getPath();

        binding = ActivityTestMultiMicBinding.inflate(getLayoutInflater());

        setContentView(binding.getRoot());

        checkPermissions(new String[]{android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
                Manifest.permission.RECORD_AUDIO});

        initListeners();
        initRecyclerView();
    }

    private void initListeners() {

        binding.start.setOnClickListener(v -> {
            DebugLog.e("clickStart");

            audioRate = getDefaultPcmConfig().rate;
            channels = getDefaultPcmConfig().channels;

            binding.recyclerView.setVisibility(View.INVISIBLE);
            binding.start.setEnabled(false);
            binding.cbMicArray.setEnabled(false);
            binding.stop.setEnabled(true);

            startRecorder();
        });

        binding.stop.setOnClickListener(v -> {
            DebugLog.e("clickStop");
            stopRecorder();

            binding.recyclerView.setVisibility(View.VISIBLE);
            binding.start.setEnabled(true);
            binding.cbMicArray.setEnabled(true);

            configPlayItems();
        });
    }

    private void initRecyclerView() {
        binding.recyclerView.setLayoutManager(new GridLayoutManager(this, 4, RecyclerView.VERTICAL, false));
        adapter = new BaseQuickAdapter<PlayItem, DataBindingHolder<ItemMicPlayMonoBinding>>() {

            @NonNull
            @Override
            protected DataBindingHolder<ItemMicPlayMonoBinding> onCreateViewHolder(@NonNull Context context, @NonNull ViewGroup parent, int viewType) {
                return new DataBindingHolder<>(R.layout.item_mic_play_mono, parent);
            }

            @Override
            protected void onBindViewHolder(@NonNull DataBindingHolder<ItemMicPlayMonoBinding> holder, int position, @Nullable PlayItem item) {
                if (item == null) return;

                ItemMicPlayMonoBinding binding = holder.getBinding();
                String text = getString(R.string.mic_play_index, position);
                binding.play.setText(text);
            }

        };
        binding.recyclerView.setAdapter(adapter);

        adapter.setOnItemClickListener((baseQuickAdapter, view, position) -> {
            PlayItem item = baseQuickAdapter.getItem(position);
            if (item != null) {
                playWavFile(item.wavFile);
                String text = getString(R.string.mic_play_index, position);
                binding.info.setText(getString(R.string.mic_status_playing_mic_sound, text));
            }
        });
    }

    private void playWavFile(File wavFile) {
        AudioManager am = getSystemService(AudioManager.class);
        int curVol = am.getStreamVolume(AudioManager.STREAM_MUSIC);
        int maxVolume = am.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
        am.setStreamVolume(AudioManager.STREAM_MUSIC, maxVolume, 0);

        try {
            MediaPlayer player = new MediaPlayer();
            player.setDataSource(wavFile.getPath());
            player.prepare();
            player.start();
            player.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
                @Override
                public void onCompletion(MediaPlayer mp) {
                    am.setStreamVolume(AudioManager.STREAM_MUSIC, curVol, 0);
                }
            });
        } catch (Exception e) {
            DebugLog.e(e.getMessage());
        }
    }

    private void configPlayItems() {
        List<PlayItem> items = new ArrayList<>();
        for (int i = 0; i < channels; i++) {
            PlayItem item = new PlayItem(fileMono(i));
            items.add(item);
        }
        adapter.submitList(items);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }

    private boolean checkPermissions(String[] permissions) {
        int result;
        List<String> permissionsNeeded = new ArrayList<>();

        for (String permission : permissions) {
            result = ContextCompat.checkSelfPermission(this, permission);
            if (result != PackageManager.PERMISSION_GRANTED) {
                permissionsNeeded.add(permission);
            }
        }

        if (!permissionsNeeded.isEmpty()) {
            ActivityCompat.requestPermissions(this, permissionsNeeded.toArray(new String[0]), 100);
            return false;
        }

        return true;
    }

    private void setupRecorder() {
        recorder = OmRecorder.wav(
                new PullTransport.Default(mic(), new PullTransport.OnAudioChunkPulledListener() {
                    @Override
                    public void onAudioChunkPulled(AudioChunk audioChunk) {
                    }
                }).setOnAudioChunkWritingListener(new PullTransport.OnAudioChunkWritingListener() {
                    @Override
                    public void onAudioChunkWriting(AudioChunk audioChunk) {
                        //单独解析audio pcm 为各个通道pcm的话使用时间太长了. 测试解析3M左右文件, 用时有20s;
                        ByteBuffer byteBuffer = ByteBuffer.wrap(audioChunk.toBytes());
                        writeAudioChunk(byteBuffer);
                    }
                }), file());

    }

    private void startRecorder() {
        if (recorder != null) {
            return;
        }

        setMicArrayEnabled(true);
        setupRecorder();

        PCMConfig pcmConfig = parseParameters(getParametersOfMicArray());
        if (audioRate != pcmConfig.rate || channels != pcmConfig.channels) {
            audioRate = pcmConfig.rate;
            channels = pcmConfig.channels;
            stopRecorder();
            setupRecorder();
        }
        initMonoOutputStream(pcmConfig);
        recorder.startRecording();
        binding.info.setText(R.string.mic_status_recording);
    }

    private void stopRecorder() {
        if (recorder == null) {
            return;
        }

        try {
            recorder.stopRecording();
            recorder = null;
        } catch (Exception e) {
            DebugLog.e(e.getMessage());
        }

        deinitMonoOutputStream();
        setMicArrayEnabled(false);
        binding.info.setText(R.string.stop_recorder);
    }

    private PullableSource mic() {
        return new PullableSource.Default(
                new AudioRecordConfig.Default(
                        MediaRecorder.AudioSource.MIC, AudioFormat.ENCODING_PCM_16BIT,
                        AudioFormat.CHANNEL_IN_STEREO, channels, audioRate
                )
        );
    }

    private AudioRecordConfig micMonoConfig() {
        /* ENCODING_PCM_FLOAT 解析用的*/
        int audioFormat = isMicArray() ? AudioFormat.ENCODING_PCM_FLOAT : AudioFormat.ENCODING_PCM_16BIT;
        return new AudioRecordConfig.Default(
                MediaRecorder.AudioSource.MIC, audioFormat,
                AudioFormat.CHANNEL_IN_MONO, 1, audioRate);
    }

    @NonNull
    private File file() {
        return new File(APP_PATH, "test_mic.wav");
    }

    @NonNull
    private File fileMono(int channelIndex) {
        return new File(APP_PATH, "test_mic-channel_" + channelIndex + ".wav");
    }

    @NonNull
    private File fileMonoCheck(int channelIndex) {
        return new File(APP_PATH, "test_mic_check-channel_" + channelIndex + ".pcm");
    }

    private void writeAudioChunk(ByteBuffer byteBuffer) {
        if (pcmConfig == null || byteBuffer == null) {
            return;
        }
        int n;
        ByteBuffer[] bbs = new ByteBuffer[pcmConfig.channels];
        ByteBuffer[] bbsCheck = new ByteBuffer[pcmConfig.channels];
        try {

            int capacity = byteBuffer.capacity() / bbs.length;
            for (int i = 0; i < bbs.length; i++) {
                bbs[i] = ByteBuffer.allocate(capacity);
                bbsCheck[i] = ByteBuffer.allocate(capacity / pcmConfig.toFormatWidth());
            }
            byte[] val = new byte[pcmConfig.toFormatWidth()];
            byte valCheck;
            for (n = 0; n < byteBuffer.array().length / val.length; n++) {
                byteBuffer.get(val);
                bbs[n % pcmConfig.channels].put(val);

                valCheck = val[val.length - 1];
                bbsCheck[n % pcmConfig.channels].put(valCheck);
            }
            for (ByteBuffer bb : bbs) {
                bb.flip();
            }

            for (ByteBuffer bb : bbsCheck) {
                bb.flip();
            }
            for (int ch = 0; ch < pcmConfig.channels; ch++) {
                outs[ch].write(bbs[ch].array());
                outsCheck[ch].write(bbsCheck[ch].array());
            }

        } catch (Exception e) {
            DebugLog.e(e.getMessage());
        }
    }

    private void initMonoOutputStream(@NonNull PCMConfig pcmConfig) {
        this.pcmConfig = pcmConfig;
        outs = new OutputStream[pcmConfig.channels];
        outsCheck = new OutputStream[pcmConfig.channels];
        for (int n = 0; n < pcmConfig.channels; n++) {
            try {
                outs[n] = Files.newOutputStream(fileMono(n).toPath());
                outsCheck[n] = Files.newOutputStream(fileMonoCheck(n).toPath());
            } catch (IOException e) {
                DebugLog.e(e.getMessage());
            }
        }
    }

    private void deinitMonoOutputStream() {
        if (pcmConfig == null) {
            return;
        }
        for (int n = 0; n < pcmConfig.channels; n++) {
            if (outs[n] == null) {
                continue;
            }
            try {
                outs[n].flush();
                outs[n].close();

                outsCheck[n].flush();
                outsCheck[n].close();

                writeWavHeader(fileMono(n));

            } catch (Exception e) {
                DebugLog.e(e.getMessage());
            }
            outs[n] = null;
            outsCheck[n] = null;
        }

        Map<String, Integer> map = new HashMap<>();
        for (int n = 0; n < pcmConfig.channels; n++) {
            File file = fileMonoCheck(n);
            String fileMD5ToString = FileUtils.getFileMD5ToString(file);

            DebugLog.e("channel-" + n + ", file is " + file.getName() + ", MD5 is " + fileMD5ToString);
            map.put(fileMD5ToString, n);
        }

        if (map.size() != pcmConfig.channels) {
            DebugLog.e("map.size() is " + map.size() + ", mic is error!");
            binding.info.setText(R.string.mic_error);
        }

        outs = null;
        outsCheck = null;
        pcmConfig = null;
    }


    private void writeWavHeader(File file) throws IOException {
        final RandomAccessFile wavFile = randomAccessFile(file);
        wavFile.seek(0); // to the beginning
        wavFile.write(new WavHeader(micMonoConfig(), file.length()).toBytes());
        wavFile.close();
    }

    private RandomAccessFile randomAccessFile(File file) {
        RandomAccessFile randomAccessFile;
        try {
            randomAccessFile = new RandomAccessFile(file, "rw");
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }
        return randomAccessFile;
    }

    //channels=%d;rate=%d;format=%d
    @NonNull
    private PCMConfig parseParameters(@Nullable String parameters) {
        PCMConfig defaultPcmConfig = getDefaultPcmConfig();
        if (TextUtils.isEmpty(parameters)) {
            return defaultPcmConfig;
        }
        int channels = defaultPcmConfig.channels;
        int rate = defaultPcmConfig.rate;
        int format = defaultPcmConfig.format;
        String[] split = parameters.split(";");
        for (String pv : split) {
            String[] kv = pv.split("=");
            if (kv.length == 2) {
                switch (kv[0]) {
                    case "channels":
                        channels = parseInt(kv[1], channels);
                        break;
                    case "rate":
                        rate = parseInt(kv[1], rate);
                        break;
                    case "format":
                        format = parseInt(kv[1], format);
                        break;
                }
            }
        }

        return new PCMConfig(channels, rate, format);
    }

    private int parseInt(String str, int def) {
        try {
            return Integer.parseInt(str);
        } catch (Exception e) {
            return def;
        }
    }

    @NonNull
    private Context getContext() {
        return this;
    }

    private boolean isMicArray() {
        return binding.cbMicArray.isChecked();
    }

    private void setMicArrayEnabled(boolean enabled) {
        if (!isMicArray()) {
            return;
        }
        MicArrayUtils.setMicArrayEnabled(getContext(), enabled);
    }

    private String getParametersOfMicArray() {
        if (!isMicArray()) {
            return "";
        }

        return MicArrayUtils.getParametersOfMicArray(getContext());
    }

    @NonNull
    private PCMConfig getDefaultPcmConfig() {
        return isMicArray() ?
                DEFAULT_PCM_CONFIG_4CH_32K_32B : DEFAULT_PCM_CONFIG_2CH_16K_16B;
    }

    private final static class PlayItem {
        @NonNull
        final File wavFile;

        private PlayItem(@NonNull File wavFile) {
            this.wavFile = wavFile;
        }
    }

    /**
     * 需要在 AndroidManifest 里配置 android.permission.MODIFY_AUDIO_SETTINGS
     */
    public static class MicArrayUtils {
        //////////////////
        private final static String AUDIO_PARAMETER_KEY_MIC_ARRAY = "mic_array";
        private final static String AUDIO_PARAMETER_VALUE_ON = "on";
        private final static String AUDIO_PARAMETER_VALUE_OFF = "off";
        private final static String PARAMETERS_MIC_ARRAY_ON = AUDIO_PARAMETER_KEY_MIC_ARRAY + "=" + AUDIO_PARAMETER_VALUE_ON;
        private final static String PARAMETERS_MIC_ARRAY_OFF = AUDIO_PARAMETER_KEY_MIC_ARRAY + "=" + AUDIO_PARAMETER_VALUE_OFF;
        ///////////////

        public static void setMicArrayEnabled(@NonNull Context context, boolean enabled) {
            AudioManager audioManager = context.getSystemService(AudioManager.class);
            audioManager.setParameters(enabled ? PARAMETERS_MIC_ARRAY_ON : PARAMETERS_MIC_ARRAY_OFF);
        }

        public static String getParametersOfMicArray(@NonNull Context context) {
            return context.getSystemService(AudioManager.class).getParameters(AUDIO_PARAMETER_KEY_MIC_ARRAY);
        }
    }

    //channels=%d;rate=%d;format=%d
    private static class PCMConfig {
        final int channels;
        final int rate;
        final int format;

        private PCMConfig(int channels, int rate, int format) {
            this.channels = channels;
            this.rate = rate;
            this.format = format;
        }

        //format 占了多少 byte
        public int toFormatWidth() {
            return format >> 3;// 32bit 占 4 个 byte
        }

        @NonNull
        @Override
        public String toString() {
            return "PCMConfig{" +
                    "channels=" + channels +
                    ", rate=" + rate +
                    ", format=" + format +
                    '}';
        }
    }
}
